Skip to content

Fix #443: tensor symbol identity includes dim and rank - #458

Merged
petlenz merged 4 commits into
mainfrom
fix-443-tensor-symbol-shape
Sep 20, 2026
Merged

petlenz merged 4 commits into
mainfrom
fix-443-tensor-symbol-shape

Conversation

@petlenz

@petlenz petlenz commented Sep 17, 2026

Copy link
Copy Markdown
Member

Closes #443.

A tensor symbol's identity covered only its name, so the same name at another shape was the same symbol:

Probe Before After
A(3,2) == A(2,2) true false
map holding A(3,2), A(2,2), A(3,4) size 1 size 3
bind A(3,2)→I and A(2,2)→7·I, then apply(A(3,2)) 7.0 1.0
trace(A(2,2)) with the 2×2 binding 14 14 (unchanged, correct)

Change. symbol_base's operator== / operator< compare the shape after the name, through a requires-guarded detail::symbol_shape helper that yields {dim, rank} for symbols whose base carries them and {0,0} otherwise. Scalar symbols are unaffected.

Extending symbol_base rather than overriding in tensor keeps symbol identity in one place: an override would have to restate the name and hash rules, and the last change to them (#348's name tiebreak) would then have needed mirroring in two files — the maintenance hazard behind this bug family.

The hash is deliberately untouched. It remains a name-only fast reject, so A(3,2) and A(2,2) still hash alike and are separated by the full comparison. Verified unchanged: hash-driven print order (pow(x,2)*y*z*X), tensor_scalar_mul(const,T).hash == T.hash, tensor_pow(T,const).hash == T.hash, and all 27 hash tests.

Tests (SymbolIdentity, beside the #348 cases): TensorShapeDistinguishesSameName covers the four probes plus ordering totality, incomparability of equal symbols, and a 3-entry map; EvaluatorKeepsBindingsPerTensorShape binds both shapes in one tensor_evaluator and asserts each returns its own data, with a trace check through the t2s evaluator.

Negative control: against main's symbol_base.h both tests fail (restored with git show, rebuilt, fresh binary) in Debug and Release; both pass with the fix. Unlike the sibling #348, Release does not crash here — the evaluator dispatches on the bound data's own shape, so the failure is silently wrong numbers.

Suites: Debug 2407/2407, Release 2407/2407 (gcc-14, -Werror minus the pedantic/deprecated-declarations warnings already on main). clang-format-18 clean.

Scope notes. Scalar symbols carry no shape-like state, and there is no t2s symbol type (t2s wraps a scalar expression), so nothing analogous is excluded elsewhere. The parser was never exposed: symbol_table already raises redeclaration_error when a name reappears with a different (rank, dim) — this was reachable only through the C++ API.

Review finding — a second silent wrong result this also fixes. A(3,2) * A(3,4) collapsed to pow(A,2) on main: a rank-2 and a rank-4 tensor merged as one factor, because the like-term path compared symbols by name only. With shape in the identity it correctly stays A*A. Tensor multiplication validates dim but not rank (#438 covers +/-), so that path stays reachable — now pinned by SymbolIdentity.MixedRankFactorsDoNotMerge, which fails against main's identity.

Review fix — shape is compared where the shape lives. The first version put the comparison in symbol_base, which had to detect dim()/rank() with a requires clause: the base of every symbol then knew about state only a tensor carries. tensor now defines its own ==, !=, < and >; equals_same_type/less_than_same_type cast to the derived type before comparing, so these win by exact match and every path reaches them. symbol_base is back to name-only identity with a comment pointing at the extension mechanism.

No virtual was needed: the dispatch already goes through the derived type. Negative control: with tensor.h restored from main, the three shape tests fail while the two cross-domain tests still pass — confirming the shape logic now lives entirely in tensor. Suite 2408/2408.

A tensor symbol compared equal to any symbol of the same name, so A(3,2)
and A(2,2) shared one key: a map held one entry for three shapes, and an
evaluator holding both bindings served whichever it stored first.

Equality and ordering now compare the shape after the name, via a
requires-guarded helper so scalar symbols (which carry no shape) are
unaffected. The hash is deliberately unchanged — it stays a name-only
fast reject, so hash-driven print order and the tensor_scalar_mul /
tensor_pow hash invariants are untouched.

Signed-off-by: petlenz <[email protected]>
A(3,2) * A(3,4) collapsed to pow(A,2) while symbol identity ignored shape.
Tensor multiplication validates dim only, so the like-term path is reachable
and deserves its own lock-in.

Signed-off-by: petlenz <[email protected]>
symbol_base had to detect dim() and rank() to compare them, so the base of
every symbol knew what only a tensor carries. tensor now defines its own
comparison operators; equals_same_type casts to the derived type first, so
they win by exact match and every path reaches them.

Signed-off-by: petlenz <[email protected]>
@petlenz
petlenz merged commit e05b3e0 into main Sep 20, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tensor symbols with the same name but different dim/rank are one key — evaluator returns the wrong tensor

1 participant